#!/usr/bin/env python
import sys
import random
import pickle   #for Ranking
import datetime #for Ranking

class Menu(object):
   def __init__(self, arena, ranking):
      self.quit = False
      self.options = {
                      'n': ('Play Game', arena.play),
                      'r': ('Ranking', ranking.show),
                      'q': ('Quit', self.close)
                     }
   def ask(self):
      print ''
      print '-=[ Pylchemist ]=-'
      for item in self.options:
         print '[%s] %s' % (item, self.options[item][0])
      sel = raw_input('--> ')
      if sel in self.options.keys():
         self.options[sel][1]()
      else:
         print 'Unknown option'

   def close(self):
      self.quit = True

class Arena(object):
   def __init__(self, gamefield=None, atoms=None):
      self.game_over = False
      self.in_pause = False
      self.gamefield = gamefield or Gamefield()
      self.atoms = atoms or Atoms_set().all()
      self.status_line = Status_line()
      self.score = Score()
      self.options = {
                      's': {'help': 'Move Left',    'run': self.move_left},
                      'f': {'help': 'Move Right',   'run': self.move_right},
                      'e': {'help': 'Rotate cw',    'run': self.rotate_cw},
                      'd': {'help': 'Rotate ccw',   'run': self.rotate_ccw},
                      ' ': {'help': 'Drop',         'run': self.drop_piece},
                      'p': {'help': 'Pause game',   'run': self.pause_game},
                      'r': {'help': 'Restart game', 'run': self.restart_game},
                      'n': {'help': 'New game',     'run': self.new_game},
                      'h': {'help': 'Help',         'run': self.help},
                     }

   def new_game(self):
      "Starts a new game"
      self.seed = random.randint(0, 1000000000)
      random.seed(self.seed)
      self.gamefield.reset()
      self.game_over = False
      self.in_pause = False
      self.higher_atom = 3
      self.set_next_piece()
      self.set_current_piece()
      self.score.reset()
      self.status_line.message = u'Starting new game'

   def set_next_piece(self):
      "Creates a new piece and sets it as next"
      atoms = [random.choice(self.atoms[:self.higher_atom]) for x in range(2)]
      self.next_piece = Piece(atoms)
      return self.next_piece

   def set_current_piece(self, col=None):
      "Sets the next_piece as current_piece and generates a new next_piece"
      if col is None:
         self.current_piece_col = self.gamefield.w / 2
      self.current_piece = self.next_piece
      self.set_next_piece()
      return self.current_piece

   def play(self):
      if not self.in_pause:
         self.new_game()
      self.in_pause = False
      while not (self.game_over or self.in_pause):
         self.show()
         self.ask()
      if self.game_over:
         self.do_game_over()

   def restart_game(self):
      "Restarts current game"
      random.seed(self.seed)
      self.game_over = False
      self.in_pause = False
      self.gamefield.reset()
      self.set_next_piece()
      self.set_current_piece()
      self.score.reset()

   def show(self):
      "Shows the current game"
      print '\r\n', '=' * self.gamefield.w * 2
      self.show_next_piece()
      print '.' * self.gamefield.w * 2
      self.show_current_piece()
      self.gamefield.show()
      print '.' * (self.gamefield.w * 2)
      print u'>[%s]%s <' % (self.score, self.status_line)
      print '=' * (self.gamefield.w * 2)

   def show_next_piece(self):
      "Shows the upcoming piece"
      print 'Next: ', u''.join([unicode(x) for x in self.next_piece.atoms]) 

   def show_current_piece(self):
      "Shows the current piece"
      for l in unicode(self.current_piece).splitlines():
         offset = self.gamefield.off_w + self.current_piece.w * self.current_piece_col
         print u' ' * offset + l
   
   def ask(self):
      "Ask the player for a move"
      for a in raw_input('Now what? '):
         if a in self.options:
            self.options.get(a)['run']()
         else:
            self.options['h']['run']()
            break

   def move_left(self):
      self.current_piece_col = max(0, self.current_piece_col - 1)
      self.prevent_overflow()

   def move_right(self):
      self.current_piece_col = min(self.gamefield.w - 1, self.current_piece_col + 1)
      self.prevent_overflow()

   def rotate_cw(self):
      self.current_piece.rotate_cw()
      self.prevent_overflow()

   def rotate_ccw(self):
      self.current_piece.rotate_ccw()
      self.prevent_overflow()
   
   def prevent_overflow(self):
      "Moves the piece to keep it inside the gamefield area"
      piece = self.current_piece 
      max_col = self.current_piece_col + len(piece.atoms)
      if piece.orient == -1 and max_col > self.gamefield.w:
         self.current_piece_col = self.gamefield.w - len(piece.atoms)
         
   def drop_piece(self):
      fit = True
      for i in range(len(self.current_piece.atoms) - 1, -1, -1):
         a_col = self.current_piece_col
         if self.current_piece.orient == -1: a_col += i       #Horizontal piece
         new_atom_row = self.gamefield.drop_atom(self.current_piece.atoms[i], a_col)
         fit = fit and (new_atom_row >= 0)
         if new_atom_row >= 0:
            self.merge_atoms(a_col, new_atom_row)
      if fit:
         self.score.add(self.current_piece.get_points())
      else:
         self.game_over = True
      self.set_current_piece()

   def get_same_level(self, col, row):
      "Returns a list of tuples with the coordinates of same level adjacents atoms"
      #this version just looks for atoms in the same row, just for testing
      #this will be replaced with a flood fill algorithm
      result = []
      target_row = self.gamefield.rows[row]
      target_level = target_row[col].level
      to_left = xrange(col - 1, -1, -1)
      to_right = xrange(col, self.gamefield.w)
      for seq in to_left, to_right:
         for i in seq:
            if target_row[i].level == target_level:
               result.append((i, row))
            else:
               break
      return result

   def merge_atoms(self, col, row):
      "Merge adjacent atoms together and creates higher level ones"
      same_level = self.get_same_level(col, row)
      if len(same_level) > 2:
         new_place = self.get_new_place(same_level)
         new_level = min(self.gamefield.rows[row][col].level + 1, len(self.atoms))
         self.score.add(2 ** new_level)
         self.status_line.message = u'New level %s atom created' % new_level
         if new_level < len(self.atoms):
            if new_level > self.higher_atom: self.higher_atom = new_level
            new_atom = self.atoms[new_level]
            self.gamefield.put_atom(new_place[0], new_place[1], new_atom)
            for coord in same_level:
               if coord != new_place:
                  self.gamefield.clear(coord[0], coord[1])
                  moved = self.gamefield.collapse()
            for m in moved:
               self.merge_atoms(m[0], m[1])
         else:
            self.gamefield.reset()

   def get_new_place(self, coords):
      "Given a set of coordinates returns the place where the new atom should be created"
      res = (self.gamefield.w, 0)
      for coord in coords:
         if coord[1] > res[1] or (coord[1] == res[1] and coord[0] < res[0]):
            res = coord
      return res

   def pause_game(self):
      self.in_pause = True

   def help(self):
      print '\r\n'.join(['%s: %s' % (x, self.options[x]['help'])
                         for x in self.options.keys()])

   def is_game_over(self):
      return False

   def do_game_over(self):
      print u'\r\n_Game Over_\r\n'

   def fill_gamefield(self, cant=None):
      "Fills the gamefield with random pieces up to cant rows"
      if cant is None:
         cant = random.randint(1, self.gamefield.h-1)
      to_fill = range(self.gamefield.h - cant, self.gamefield.h)
      for h in to_fill:
         for w in range(self.gamefield.w):
            self.gamefield.put_atom(w, h, random.choice(self.atoms))
      return self.gamefield.rows

   def load_gamefield(self, atoms):
      "Given a string with digits loads a gamefield with atoms of the corresponding level"
      self.gamefield.reset()
      for h, row in enumerate(atoms.splitlines()):
         for w, l in enumerate(row):
            if l.isdigit() and int(l) >= 0:
               self.gamefield.put_atom(w, h, self.atoms[int(l)])

class Gamefield(object):
   "The field where the player plays the pieces"
   def __init__(self, w=6, h=7):
      self.step = 2
      self.w = w                        #number of columns in the gamefield
      self.h = h                        #number of rows in the gamefield
      self.total_w = (self.w + 1) * 2   #total width of the gamefield
      self.off_w = 1                    #horizontal offset
      self.reset()

   class Empty_space(object):
      "One empty cell of the gamefield"
      def __init__(self):
         self.level = -1

      def __repr__(self):
         return u'Empty space'

      def __unicode__(self):
         return u'  '

   def reset(self):
      "Clears the game field"
      self.rows = [[self.Empty_space()] * self.w for i in range(self.h)]
      return self.rows

   def show(self):
      "Shows the game field"
      print    u'+' + u''.join([u'%s-' % x for x in range(self.w)]) + u'+'
      for h, row in enumerate(self.rows):
         print u'|' + u''.join([unicode(x) for x in row])      + u'|%s' % h
      print    u'+' + u''.join([u'%s-' % x for x in range(self.w)]) + u'+'

   def get_col_top(self, col):
      "Return the first empty cell from top to bottom in the column"
      h = 0
      while h < self.h and self.rows[h][col].level < 0:
         h += 1
      return h - 1

   def drop_atom(self, atom, col):
      "Drops an atom into the gamefield"
      row = self.get_col_top(col)
      if row >= 0:
         self.put_atom(col, row, atom)
      return row

   def put_atom(self, w, h, atom):
      "Places an atom in the gamefield"
      self.rows[h][w] = atom

   def clear(self, w, h):
      "Clears the gamefield cell putting an Empy_space on it"
      self.rows[h][w] = self.Empty_space()

   def collapse(self):
      "Move down the atoms with empty space below them"
      moved = set()
      for h in range(self.h - 1, 0, -1):
         for w, cell in enumerate(self.rows[h]):
            if cell.level == -1 and self.rows[h-1][w].level >= 0:
               self.rows[h][w] = self.rows[h-1][w]
               self.rows[h-1][w] = self.Empty_space()
               moved.add((w,h))
      return moved

class Atoms_set(object):
   "Set of all posible atoms"
   #props defines all possible properties for all possible atoms
   #the keys are the atom's level, higher level atoms are created using lower level atoms
   props = {
            0: {'color': '31', 'char':   u'a '},
            1: {'color': '32', 'char':   u'b '},
            2: {'color': '1;33', 'char': u'c '},
            3: {'color': '34', 'char':   u'd '},
            4: {'color': '35', 'char':   u'e '},
            5: {'color': '36', 'char':   u'f '}
           }
   
   def get_level(self, level):
      "Return atom of given level"
      return self.Atom(level, self.props[level])
   
   def get_random(self, max_level=None):
      "Returns a random atom from the set"
      if max_level is None:
         max_level = max(self.props.keys())
      l = min(max_level, random.choice(self.props.keys()))
      return self.Atom(l, self.props[l])

   def all(self):
      "Returns a list with all posible atoms"
      return [self.Atom(l, p) for l, p in self.props.items()]

   class Atom(object):
      "The most basic game element which must be combined to create higher level atoms"
      def __init__(self, level, props):
         self.level = level
         self.color = props['color']
         self.char = props['char']
         self.w = len(self.char)

      def __repr__(self):
         return u'Level %s atom' % self.level

      def __unicode__(self):
         return u'\033[%sm%s\033[0m' % (self.color, self.char)

class Piece(object):
   "A set of atoms controled by the user"
   def __init__(self, atoms):
      self.atoms = atoms
      self.orient = 1                            #1: vertical, -1: horizontal
      self.w = max([x.w for x in self.atoms])   #width of the piece

   def __repr__(self):
      return 'Piece of (%s)' % ', '.join([str(x) for x in self.atoms])

   def __unicode__(self):
      glue = u''
      if self.orient == 1:
         glue = u'\r\n'
      return glue.join([unicode(x) for x in self.atoms])

   def rotate_cw(self):
      "Rotates the piece clockwise"
      if self.orient == 1:
         self.atoms.reverse()
      self.orient *= -1
      return self.atoms

   def rotate_ccw(self):
      "Rotates the piece counter-clockwise"
      if self.orient == -1:
         self.atoms.reverse()
      self.orient *= -1
      return self.atoms

   def get_points(self):
      "Returns the points the player gets when drops the piece"
      return sum([x.level for x in self.atoms])

class Ranking(object):
   "Top scores"
   def __init__(self, fname='top_scores'):
      self.fname = fname

   def load_scores(self):
      "Loads the score data from a file"
      file = open(self.fname, 'r')
      data = pickle.load(file)
      file.close()
      return data

   def write_scores(self, data):
      "Writes the data to the scores file"
      file = open(self.fname, 'w')
      pickle.dump(data, file)
      file.close()

   def reset_scores(self):
      "Reset the scores files. Warning, it deletes all scores in the file"
      file = open(self.fname, 'w')
      pickle.dump([], file)
      file.close()

   def update_pos(self, data=None):
      "Sorts the scores and updates pos attribute"
      if data is None:
         data = self.load_scores()
      data.sort(key=lambda x: x['score'], reverse=True)
      for i in range(len(data)):
         data[i]['pos'] = i + 1
      return data

   def add(self, score, pieces, name=u'no name'):
      "Adds a new score to the ranking"
      data = self.load_scores()
      time = unicode(datetime.datetime.now().strftime('%x %X'))
      new_score = {'pos': 0, 'score': score, 'pieces': pieces,
                   'name': name, 'datetime': time}
      data.append(new_score)
      data = self.update_pos(data)
      self.write_scores(data)
      return data

   def show(self, top=16):
      "Shows the top scores"
      data = self.load_scores()
      if top == 0: top = len(data)
      header = u' # Name                 Score   Pieces Date'
      lines = [u'%(pos)02d %(name)-20s %(score)07d  %(pieces)04d  %(datetime)s' % x
               for x in data[:top]]
      scores = u'\n'.join(lines)
      print header + '\n' + scores

class Status_line(object):
   "A brief information to the player about the current game status"
   def __init__(self):
      self.message = u''
   
   def __unicode__(self):
      message = self.message
      self.message = u''
      return message

class Score(object):
   "Keeps the current game score"
   def __init__(self):
      self.reset()

   def __unicode__(self):
      return u'%s' % self.points

   def reset(self):
      self.points = 0
      self.bonus = 0
      self.multi = 1

   def add(self, points, bonus=0):
      "Adds points to the score"
      self.points += points
      self.bonus += bonus

def start(argv=None):
   if argv is None:
      argv = sys.argv
   arena = Arena()
   ranking = Ranking()
   menu = Menu(arena, ranking)
   while not menu.quit:
      menu.ask()
   print 'Thanks for playing'
   return 0

if __name__ == '__main__':
   sys.exit(start())
